use pollster::FutureExt;
use wgpu::SurfaceTarget;

use crate::Scene;

use super::shader::Shader;

pub struct Context<'window> {
    pub device: wgpu::Device,
    pub queue: wgpu::Queue,
    pub surface: wgpu::Surface<'window>,
    pub config: wgpu::SurfaceConfiguration,
}

impl<'w> Context<'w> {
    pub fn new(window: impl Into<SurfaceTarget<'w>>, width: u32, height: u32) -> Self {
        use wgpu::*;
        let instance = Instance::default();

        let surface = instance.create_surface(window).unwrap();

        let adapter = instance
            .request_adapter(&RequestAdapterOptions {
                compatible_surface: Some(&surface),
                ..Default::default()
            })
            .block_on()
            .unwrap();

        let (device, queue) = adapter
            .request_device(&DeviceDescriptor::default(), None)
            .block_on()
            .unwrap();

        let config = surface.get_default_config(&adapter, width, height).unwrap();
        surface.configure(&device, &config);

        Self {
            surface,
            device,
            queue,
            config,
        }
    }

    pub fn resize(&mut self, width: u32, height: u32) {
        self.config.width = width;
        self.config.height = height;
        self.surface.configure(&self.device, &self.config);
    }

    pub fn current_texture(&self) -> wgpu::SurfaceTexture {
        self.surface.get_current_texture().unwrap()
    }

    pub fn screen_view(&self, surf_tex: &wgpu::SurfaceTexture) -> wgpu::TextureView {
        surf_tex.texture.create_view(&wgpu::TextureViewDescriptor {
            format: Some(self.config.format),
            dimension: Some(wgpu::TextureViewDimension::D2),
            ..Default::default()
        })
    }

    pub fn render<F: FnMut(wgpu::TextureView, &mut wgpu::CommandEncoder)>(&self, mut render_func: F) {
        let surf_tex = self.current_texture();
        let mut encoder = self.create_command_encoder();

        render_func(self.screen_view(&surf_tex), &mut encoder);

        self.queue.submit([encoder.finish()]);
        surf_tex.present();
    }

    pub fn render_scene(&self, scene: &mut Scene) {
        scene.prepare(self);

        self.render(|view, encoder| {
            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: None,
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(scene.clear_color()),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
            });

            scene.render(&mut pass);
        })
    }

    pub fn create_command_encoder(&self) -> wgpu::CommandEncoder {
        self.device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None })
    }

    pub fn create_bind_group_layout(
        &self,
        entries: &[wgpu::BindGroupLayoutEntry],
    ) -> wgpu::BindGroupLayout {
        self.device
            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
                label: None,
                entries,
            })
    }

    pub fn create_pipeline_layout(
        &self,
        bind_group_layouts: &[&wgpu::BindGroupLayout],
    ) -> wgpu::PipelineLayout {
        self.device
            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
                label: None,
                bind_group_layouts,
                push_constant_ranges: &[],
            })
    }

    pub fn create_simple_render_pipeline(
        &self,
        layout: &wgpu::PipelineLayout,
        shader: &Shader,
        primitive: wgpu::PrimitiveState,
    ) -> wgpu::RenderPipeline {
        self.device
            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
                label: None,
                layout: Some(layout),
                vertex: shader.vertex(),
                primitive,
                depth_stencil: None,
                multisample: wgpu::MultisampleState {
                    count: 1,
                    mask: !0,
                    alpha_to_coverage_enabled: false,
                },
                fragment: Some(shader.fragment()),
                multiview: None,
            })
    }

    pub fn create_mesh_pipeline(
        &self,
        layouts: &[&wgpu::BindGroupLayout],
        shader: &Shader,
    ) -> wgpu::RenderPipeline {
        self.create_simple_render_pipeline(
            &self.create_pipeline_layout(layouts),
            shader,
            wgpu::PrimitiveState {
                front_face: wgpu::FrontFace::Cw,
                cull_mode: Some(wgpu::Face::Back),
                ..Default::default()
            },
        )
    }

    pub fn create_texture2d(
        &self,
        format: wgpu::TextureFormat,
        width: u32,
        height: u32,
        mip_level: u32,
        usage: wgpu::TextureUsages,
    ) -> wgpu::Texture {
        self.device.create_texture(&wgpu::TextureDescriptor {
            label: None,
            size: wgpu::Extent3d {
                width,
                height,
                depth_or_array_layers: 1,
            },
            mip_level_count: mip_level,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format,
            usage: usage | wgpu::TextureUsages::COPY_DST,
            view_formats: &[],
        })
    }
}
